package com.ihr360.job.core.scope;

import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanDefinitionVisitor;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.Ordered;
import org.springframework.util.Assert;
import org.springframework.util.StringValueResolver;

public abstract class BatchScopeSupport implements Scope, BeanFactoryPostProcessor, Ordered {

    private boolean autoProxy = true;

    private boolean proxyTargetClass = false;

    private String name;

    private int order = Ordered.LOWEST_PRECEDENCE;

    /**
     * @param order the order value to set priority of callback execution for
     * the {@link BeanFactoryPostProcessor} part of this scope bean.
     */
    public void setOrder(int order) {
        this.order = order;
    }

    @Override
    public int getOrder() {
        return order;
    }

    public String getName() {
        return this.name;
    }

    /**
     * Public setter for the name property. This can then be used as a bean
     * definition attribute, e.g. scope="job".
     *
     * @param name the name to set for this scope.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Flag to indicate that proxies should use dynamic subclassing. This allows
     * classes with no interface to be proxied. Defaults to false.
     *
     * @param proxyTargetClass set to true to have proxies created using dynamic
     * subclasses
     */
    public void setProxyTargetClass(boolean proxyTargetClass) {
        this.proxyTargetClass = proxyTargetClass;
    }

    /**
     * Flag to indicate that bean definitions need not be auto proxied. This gives control back to the declarer of the
     * bean definition (e.g. in an &#64;Configuration class).
     *
     * @param autoProxy the flag value to set (default true)
     */
    public void setAutoProxy(boolean autoProxy) {
        this.autoProxy = autoProxy;
    }

    public abstract String getTargetNamePrefix();

    /**
     * Register this scope with the enclosing BeanFactory.
     *
     * @see BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory)
     *
     * @param beanFactory the BeanFactory to register with
     * @throws BeansException if there is a problem.
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        beanFactory.registerScope(name, this);

        if(!autoProxy) {
            return;
        }

        Assert.state(beanFactory instanceof BeanDefinitionRegistry,
                "BeanFactory was not a BeanDefinitionRegistry, so JobScope cannot be used.");
        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;

        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            if (!beanName.startsWith(getTargetNamePrefix())) {
                BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
                // Replace this or any of its inner beans with scoped proxy if it
                // has this scope
                boolean scoped = name.equals(definition.getScope());
                Scopifier scopifier = new Scopifier(registry, name, proxyTargetClass, scoped);
                scopifier.visitBeanDefinition(definition);

                if (scoped && !definition.isAbstract()) {
                    createScopedProxy(beanName, definition, registry, proxyTargetClass);
                }
            }
        }

    }

    /**
     * Wrap a target bean definition in a proxy that defers initialization until
     * after the {@link StepContext} is available. Amounts to adding
     * &lt;aop-auto-proxy/&gt; to a step scoped bean.
     *
     * @param beanName the bean name to replace
     * @param definition the bean definition to replace
     * @param registry the enclosing {@link BeanDefinitionRegistry}
     * @param proxyTargetClass true if we need to force use of dynamic
     * subclasses
     * @return a {@link BeanDefinitionHolder} for the new representation of the
     * target. Caller should register it if needed to be visible at top level in
     * bean factory.
     */
    protected static BeanDefinitionHolder createScopedProxy(String beanName, BeanDefinition definition,
                                                            BeanDefinitionRegistry registry, boolean proxyTargetClass) {

        BeanDefinitionHolder proxyHolder;

        proxyHolder = ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(definition, beanName), registry,
                proxyTargetClass);

        registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition());

        return proxyHolder;

    }

    /**
     * Helper class to scan a bean definition hierarchy and force the use of
     * auto-proxy for step scoped beans.
     *
     * @author Dave Syer
     *
     */
    protected static class Scopifier extends BeanDefinitionVisitor {

        private final boolean proxyTargetClass;

        private final BeanDefinitionRegistry registry;

        private final String scope;

        private final boolean scoped;

        public Scopifier(BeanDefinitionRegistry registry, String scope, boolean proxyTargetClass, boolean scoped) {
            super(new StringValueResolver() {
                @Override
                public String resolveStringValue(String value) {
                    return value;
                }
            });
            this.registry = registry;
            this.proxyTargetClass = proxyTargetClass;
            this.scope = scope;
            this.scoped = scoped;
        }

        @Override
        protected Object resolveValue(Object value) {

            BeanDefinition definition = null;
            String beanName = null;
            if (value instanceof BeanDefinition) {
                definition = (BeanDefinition) value;
                beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry);
            }
            else if (value instanceof BeanDefinitionHolder) {
                BeanDefinitionHolder holder = (BeanDefinitionHolder) value;
                definition = holder.getBeanDefinition();
                beanName = holder.getBeanName();
            }

            if (definition != null) {
                boolean nestedScoped = scope.equals(definition.getScope());
                boolean scopeChangeRequiresProxy = !scoped && nestedScoped;
                if (scopeChangeRequiresProxy) {
                    // Exit here so that nested inner bean definitions are not
                    // analysed
                    return createScopedProxy(beanName, definition, registry, proxyTargetClass);
                }
            }

            // Nested inner bean definitions are recursively analysed here
            value = super.resolveValue(value);
            return value;

        }
    }
}